ES6開始就引入了let、const替代var去宣告變數。今天就來整理一下自己學習let和const時碰到的概念。
開發時用var宣告變數會容易導致意外汙染全域變數的問題,例如是區域變數覆蓋全域變數。
var food = 'apple';
function func(){
var result = 'I eat ' + food
console.log(result)
}
func(); //I eat apple
這個例子很簡單,在func函式裏用到全域變數food,組合字串及回傳。但如果程式碼變得更複雜時,又或者另一個開發者沒注意到food已經在第一行宣告過了,就可能會出現以下的問題:
var food = 'apple';
// 200行code之後
function func(){
var result = 'I eat ' + food
console.log(result) // I eat undefined
// 100行code之後,我忘了之前已經宣告過food
var food = 'banana';
}
func();
在func這個函式裹的最後一行程式碼,我們再次宣告food這個變數及重新賦值,根據hoisting(提升)的概念,在函式裹的var food會提升至函式作用域裹的最高處,在「提升之後(var food)」到「賦值之前food = 'banana'」,這段期間food的值會是undefined。我們可以把整個過程想像如下圖:
var food = 'apple';
function func(){
var food
// 提升之後
var result = 'I eat ' + food
console.log(result) // I eat undefined
// 賦值之前
food = 'banana';
}
func();
所以在函式裹第二行的food會變成undefined。
澄清一點,food變成了undefined這個問題,並不是因為重複宣告變數。我在函式內再次宣告在函式外的變數是不會報錯的,如下面的做法:
var food = 'apple';
// 200行code之後
function func(){
// 100行code之後,我忘了之前已經宣告過food
var food = 'banana';
var result = 'I eat ' + food
console.log(result) // I eat banana
}
func();
以上的例子,我把var food = 'banana'放在var result = 'I eat ' + food前面,這裹會成功回傳I eat banana。因為我是在變數food被賦值'banana'之後(用=去賦予),才提取food這個變數,這時候food已經是'banana'。總括來說,這個問題出現與否是取決於你在什麼時候提取這個變數。重複一次,如果你在「提升之後(var food)」到「賦值之前food = 'banana'」提取變數,就會變成undefined。
更安全的做法就是,JS不讓開發者在「提升之後」到「賦值之前」的這段期間提取變數。如果開發者要這樣做,JS就直接報錯,而不是拋出一個undefeind照讓你過關。而let和const就能正正做到這一點。下文會再重用這個例子去講解更多。
另外再舉個很多人都知道的for迴圈邪惡例子,我們預期印出0,1,2,3,4:
for (var i=0; i<5; i++){
setTimeout(function(){
console.log(i) //一秒後console顯示五次5
}, 1000);
}
但最後console是回傳五次5這個數值:
因為for迴圈的工作就是跑5次setTimeout,setTimeout裏面的事不干for迴圈的事,當它極速跑完迴圈,1秒後setTimeout才會執行,但這時候的i已經是5了,所以五次的迴圈會回傳5。
但這不是我們想要的,我們要怎樣才可以顯示沒有因為跑迴圈而被汙染的變數?我們先暫時放下這問題,之後再解決。
const甚至要求你必需賦值),就去提取該變數便會報錯,而非回傳undefined
window的屬性區塊作用域是指大括號{}之間的區域。這時候變數只存活在{}這個括號裏,{}外就不能調用。就像下面例子一樣:
{
const x = 10;
}
console.log(x) //Uncaught ReferenceError: x is not defined
{
let y = 20;
}
console.log(y) //Uncaught ReferenceError: y is not defined
{
var z = 30;
}
console.log(z) //30
再舉個例子:
let applePrice = 5
if(applePrice < 10){
let message = 'I will buy an apple'
console.log(message) //I will buy an apple
}
console.log(message) //message is not defined
回到剛剛for 迴圈沒有印出0,1,2,3,4的例子,如果改成let去宣告i,就能把每次的迴圈次數數字帶進去了。這裏的i不再是全域變數,所以不會顯示五次5:
for (let i=0; i<5; i++){
setTimeout(function(){
console.log(i)
}, 1000*i);
}
let、const、var來宣告變數,都會提升至作用域的最高處。不同的是,如果我們沒有宣告就直接使用該變數時,var會回傳undefined,但const和let會報錯。
第一點要注意的是,let、const和var一樣,都是會提升至作用域的最高處,例如有開發者曾經舉出以下例子:
var a = 10
function test(){
console.log(a) //Uncaught ReferenceError: Cannot access 'a' before initialization
let a
}
test()
如果let沒有提升,那麼console應該會顯示10。但這裹卻是ReferenceError。
const也一樣:
var a = 10
function test(){
console.log(a) //Uncaught ReferenceError: Cannot access 'a' before initialization
const a = 5;
}
test()
注意,因為const在宣告時一定要賦值,所以我寫了const a = 5,這一點之後會再提及。
從這兩個例子可見,let和const所宣告的變數是有提升至最高函式的作用域。
第二點是,let和const雖然和var一樣有提升的作用,但let和const不會預設有undefined這個值。
看看以下MDN的說法:
Unlike variables declared with var, which will start with the value undefined, let variables are not initialized until their definition is evaluated. Accessing the variable before the initialization results in a ReferenceError. The variable is in a "temporal dead zone" from the start of the block until the initialization is processed.
意思是用var宣告的變數,它的初始值預設是undefined,但let不會有這個預設。當執行let變數的宣告語句時,let才會被初始化和能夠被訪問。
如果我們這樣去宣告x,就可以得出undefined:
let x = 10
function func(){
let x
console.log(x); //undefined
}
func();
因為以上例子中,宣告x的語句先被執行,這時候x這個變數已被初始化和能夠被訪問。
但const不能只宣告但不賦值,因為ES6規定用const宣告變數時,一定要指定一個值給它:
const x = 10
function func(){
const x //Uncaught SyntaxError: Missing initializer in const declaration
console.log(x);
}
func();
看看MDN的對於const的解釋:
An initializer for a constant is required. You must specify its value in the same statement in which it's declared. (This makes sense, given that it can't be changed later.)
再來多一個例子,我們重用文章開頭,回傳I eat undefined結果的例子去改寫:
let food = 'apple';
// 200行code之後
function func(){
let result = 'I eat ' + food //Uncaught ReferenceError: Cannot access 'food' before initialization
console.log(result)
// 100行code之後,我忘了之前已經宣告過food
let food = 'banana';
}
func();
console會回傳Uncaught ReferenceError: Cannot access 'food' before initialization。直接說明我不能在food這個變數被初始化之前就去訪問,換言之,就是未執行let food這行宣告語法前,food是不能被訪問。
變數food會提升到函式作用域的最高處,而在「變數food」後,「執行let food這行宣告語法」前,這段時間我們會稱為暫時性死區(TDZ),在TDZ中,我們不能訪問food這個變數。如下:
let food = 'apple';
// 200行code之後
function func(){
//food提升到這裹,TDZ開始
let result = 'I eat ' + food //Uncaught ReferenceError: Cannot access 'food' before initialization
console.log(result)
//宣告food,TDZ結束
let food = 'banana';
}
func();
如果該變數已經被宣告過,就不能再宣告。let和const都一樣。
例子:
let food = 'banana';
let food = 'apple'; //Uncaught SyntaxError: Identifier 'food' has already been declared
window的屬性用var宣告全域變數,會成為window的屬性,但let和const不會。
很多人都知道不要用var,要改用let和const。如果該變數會變,就用let,不變就用const。
根據Google的JavaScript style guide,說明我們應該默認用const,如果該變數需要重新被賦值才用let,永遠不用var。
Declare all local variables with either const or let. Use const by default, unless a variable needs to be reassigned. The var keyword must not be used
但我在剛習慣用let和const去開發時,有時會搞不清什麼時候用let和const,到底怎樣去介定那個變數會變還是不變?例如以下例子,我會修改陣列和物件裏面的資料,這樣算是變嗎?
let x = [];
x.push(1,2,3,4)
let y = {name: 'Mary', age: 30}
y.name = 'Peter'
後來在google找到一些解答,原來要決定那個變數會變,還是不變,是指那個變數的記憶體地址有沒有變,而不是它的值。
簡單重溫一下記憶體存放變數的原則。記憶體存放變數的值時,會看變數的值是哪個資料型別:
undefined、null、symbol(ES6加入)。回到剛才提及的例子,我只是修改了該陣列和該物件裏的值,而它們在記憶體的地址都沒有被改變,所以它們並沒有變,也因為它們沒有變,所以我應該用const去宣告,而非let。
如何才算變呢?重用這兩個例子,如果我做以下的事,就是更改了它們的記憶體地址,也就是變了:
const x = [];
x = [1,2,3,4] //Uncaught TypeError: Assignment to constant variable.
const y = {name: 'Mary', age: 30}
y = {name: 'Peter'}; //Uncaught TypeError: Assignment to constant variable.
JavaScript’s Memory Model
一次說清楚 JavaScript 中宣告的各種提升行為(var、function、let/const)
JavaScript: var, let, const 差異
我知道你懂 hoisting,可是你了解到多深?
Day26 var 與 ES6 let const 差異
Var, Let, and Const – What's the Difference?